/**
 * Copyright 2013-2015, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule readRelayQueryData
 * 
 * @typechecks
 */

'use strict';

var _inherits = require('babel-runtime/helpers/inherits')['default'];

var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default'];

Object.defineProperty(exports, '__esModule', {
  value: true
});
var GraphQLStoreDataHandler = require('./GraphQLStoreDataHandler');
var GraphQLFragmentPointer = require('./GraphQLFragmentPointer');
var GraphQLStoreRangeUtils = require('./GraphQLStoreRangeUtils');
var RelayConnectionInterface = require('./RelayConnectionInterface');

var RelayProfiler = require('./RelayProfiler');
var RelayQuery = require('./RelayQuery');
var RelayQueryVisitor = require('./RelayQueryVisitor');
var RelayRecordState = require('./RelayRecordState');

var callsFromGraphQL = require('./callsFromGraphQL');
var callsToGraphQL = require('./callsToGraphQL');
var invariant = require('fbjs/lib/invariant');
var validateRelayReadQuery = require('./validateRelayReadQuery');

var EDGES = RelayConnectionInterface.EDGES;
var PAGE_INFO = RelayConnectionInterface.PAGE_INFO;

/**
 * @internal
 *
 * Retrieves data from the `RelayStore`.
 */
function readRelayQueryData(store, queryNode, dataID, options) {
  var reader = new RelayStoreReader(store, options);
  var data = reader.retrieveData(queryNode, dataID);

  // We validate only after retrieving the data, to give our `invariant`
  // checks below a chance to fail fast.
  validateRelayReadQuery(queryNode, options);

  return data;
}

var RelayStoreReader = (function (_RelayQueryVisitor) {
  _inherits(RelayStoreReader, _RelayQueryVisitor);

  function RelayStoreReader(recordStore, options) {
    _classCallCheck(this, RelayStoreReader);

    _RelayQueryVisitor.call(this);
    this._recordStore = recordStore;
    this._traverseFragmentReferences = options && options.traverseFragmentReferences || false;
    this._traverseGeneratedFields = options && options.traverseGeneratedFields || false;
  }

  /**
   * Checks that `parent` either has range calls on it or does not contain either
   * `page_info` or `edges` fields. This enforcement intentionally transcends
   * traverseFragmentReferences boundaries.
   */

  /**
   * Runs `queryNode` against the data in `dataID` and returns the result.
   */

  RelayStoreReader.prototype.retrieveData = function retrieveData(queryNode, dataID) {
    var result = {
      data: undefined,
      dataIDs: {}
    };
    var rangeData = GraphQLStoreRangeUtils.parseRangeClientID(dataID);
    var status = this._recordStore.getRecordState(rangeData ? rangeData.dataID : dataID);
    if (status === RelayRecordState.EXISTENT) {
      var state = {
        componentDataID: null,
        data: undefined,
        parent: null,
        rangeInfo: null,
        seenDataIDs: result.dataIDs,
        storeDataID: dataID
      };
      this.visit(queryNode, state);
      result.data = state.data;
    } else if (status === RelayRecordState.NONEXISTENT) {
      result.data = null;
    }
    return result;
  };

  RelayStoreReader.prototype.visitField = function visitField(node, state) {
    // Check for range client IDs (eg. `someID_first(25)`) and unpack if
    // present, overriding `state`.
    this._handleRangeInfo(node, state);

    if (!node.isScalar() || node.isGenerated()) {
      // Make sure we return at least the __dataID__.
      getDataObject(state);
    }

    if (node.isGenerated() && !this._traverseGeneratedFields) {
      return;
    }
    var rangeInfo = state.rangeInfo;
    if (rangeInfo && node.getSchemaName() === EDGES) {
      this._readEdges(node, rangeInfo, state);
    } else if (rangeInfo && node.getSchemaName() === PAGE_INFO) {
      this._readPageInfo(node, rangeInfo, state);
    } else if (node.isScalar()) {
      this._readScalar(node, state);
    } else if (node.isPlural()) {
      this._readPlural(node, state);
    } else if (node.isConnection()) {
      this._readConnection(node, state);
    } else {
      this._readLinkedField(node, state);
    }
    state.seenDataIDs[state.storeDataID] = true;
  };

  RelayStoreReader.prototype.visitFragment = function visitFragment(node, state) {
    if (node.isReferenceFragment() && !this._traverseFragmentReferences) {
      var dataID = getComponentDataID(state);
      var fragmentPointer = new GraphQLFragmentPointer(node.isPlural() ? [dataID] : dataID, node);
      this._setDataValue(state, fragmentPointer.getFragment().getConcreteFragmentID(), fragmentPointer);
    } else {
      this.traverse(node, state);
    }
  };

  RelayStoreReader.prototype._readScalar = function _readScalar(node, state) {
    var storageKey = node.getStorageKey();
    var field = this._recordStore.getField(state.storeDataID, storageKey);
    if (field === undefined) {
      return;
    } else if (field === null && !state.data) {
      state.data = null;
    } else {
      this._setDataValue(state, node.getApplicationName(), Array.isArray(field) ? field.slice() : field);
    }
  };

  RelayStoreReader.prototype._readPlural = function _readPlural(node, state) {
    var _this = this;

    var storageKey = node.getStorageKey();
    var dataIDs = this._recordStore.getLinkedRecordIDs(state.storeDataID, storageKey);
    if (dataIDs) {
      var applicationName = node.getApplicationName();
      var previousData = getDataValue(state, applicationName);
      var nextData = dataIDs.map(function (dataID, ii) {
        var data;
        if (previousData instanceof Object) {
          data = previousData[ii];
        }
        var nextState = {
          componentDataID: null,
          data: data,
          parent: node,
          rangeInfo: null,
          seenDataIDs: state.seenDataIDs,
          storeDataID: dataID
        };
        node.getChildren().forEach(function (child) {
          return _this.visit(child, nextState);
        });
        return nextState.data;
      });
      this._setDataValue(state, applicationName, nextData);
    }
  };

  RelayStoreReader.prototype._readConnection = function _readConnection(node, state) {
    var applicationName = node.getApplicationName();
    var storageKey = node.getStorageKey();
    var calls = node.getCallsWithValues();
    var dataID = this._recordStore.getLinkedRecordID(state.storeDataID, storageKey);
    if (!dataID) {
      return;
    }
    enforceRangeCalls(node);
    var metadata = this._recordStore.getRangeMetadata(dataID, calls);
    var nextState = {
      componentDataID: getConnectionClientID(node, dataID),
      data: getDataValue(state, applicationName),
      parent: node,
      rangeInfo: metadata && calls.length ? metadata : null,
      seenDataIDs: state.seenDataIDs,
      storeDataID: dataID
    };
    this.traverse(node, nextState);
    this._setDataValue(state, applicationName, nextState.data);
  };

  RelayStoreReader.prototype._readEdges = function _readEdges(node, rangeInfo, state) {
    var _this2 = this;

    var previousData = getDataValue(state, EDGES);
    var edges = rangeInfo.requestedEdges.map(function (edgeData, ii) {
      var data;
      if (previousData instanceof Object) {
        data = previousData[ii];
      }
      var nextState = {
        componentDataID: null,
        data: data,
        parent: node,
        rangeInfo: null,
        seenDataIDs: state.seenDataIDs,
        storeDataID: edgeData.edgeID
      };
      _this2.traverse(node, nextState);
      return nextState.data;
    });
    this._setDataValue(state, EDGES, edges);
  };

  RelayStoreReader.prototype._readPageInfo = function _readPageInfo(node, rangeInfo, state) {
    var _this3 = this;

    var pageInfo = rangeInfo.pageInfo;

    !pageInfo ? process.env.NODE_ENV !== 'production' ? invariant(false, 'readRelayQueryData(): Missing field, `%s`.', PAGE_INFO) : invariant(false) : undefined;
    var info = pageInfo; // for Flow
    var nextData;

    // Page info comes from the range metadata, so we do a custom traversal here
    // which is simpler than passing through page-info-related state as a hint
    // for the normal traversal.
    var read = function read(child) {
      if (child instanceof RelayQuery.Fragment) {
        if (child.isReferenceFragment() && !_this3._traverseFragmentReferences) {
          var fragmentPointer = new GraphQLFragmentPointer(getComponentDataID(state), child);
          nextData = nextData || {};
          var concreteFragmentID = fragmentPointer.getFragment().getConcreteFragmentID();
          nextData[concreteFragmentID] = fragmentPointer;
        } else {
          child.getChildren().forEach(read);
        }
      } else {
        var field = child;
        if (!field.isGenerated() || _this3._traverseGeneratedFields) {
          nextData = nextData || {};
          nextData[field.getApplicationName()] = info[field.getStorageKey()];
        }
      }
    };
    node.getChildren().forEach(read);

    this._setDataValue(state, PAGE_INFO, nextData);
  };

  RelayStoreReader.prototype._readLinkedField = function _readLinkedField(node, state) {
    var storageKey = node.getStorageKey();
    var applicationName = node.getApplicationName();
    var dataID = this._recordStore.getLinkedRecordID(state.storeDataID, storageKey);
    if (dataID == null) {
      this._setDataValue(state, applicationName, dataID);
      return;
    }
    var nextState = {
      componentDataID: null,
      data: getDataValue(state, applicationName),
      parent: node,
      rangeInfo: null,
      seenDataIDs: state.seenDataIDs,
      storeDataID: dataID
    };
    var status = this._recordStore.getRecordState(dataID);
    if (status === RelayRecordState.EXISTENT) {
      // Make sure we return at least the __dataID__.
      getDataObject(nextState);
    }
    this.traverse(node, nextState);
    this._setDataValue(state, applicationName, nextState.data);
  };

  /**
   * Assigns `value` to the property of `state.data` identified by `key`.
   *
   * Pre-populates `state` with a suitable `data` object if needed, and copies
   * over any `__status__` field, if present.
   */

  RelayStoreReader.prototype._setDataValue = function _setDataValue(state, key, value) {
    var data = getDataObject(state); // ensure __dataID__
    if (value === undefined) {
      return;
    }
    data[key] = value;

    // Copy over the status, if any.
    var status = this._recordStore.getField(state.storeDataID, '__status__');
    if (status != null) {
      data.__status__ = status;
    }
  };

  /**
   * Checks to see if we have a range client ID (eg. `someID_first(25)`), and if
   * so, unpacks the range metadata, stashing it into (and overriding) `state`.
   */

  RelayStoreReader.prototype._handleRangeInfo = function _handleRangeInfo(node, state) {
    var rangeData = GraphQLStoreRangeUtils.parseRangeClientID(state.storeDataID);
    if (rangeData != null) {
      state.componentDataID = state.storeDataID;
      state.storeDataID = rangeData.dataID;
      state.rangeInfo = this._recordStore.getRangeMetadata(state.storeDataID, callsFromGraphQL(rangeData.calls, rangeData.callValues));
    }
  };

  return RelayStoreReader;
})(RelayQueryVisitor);

function enforceRangeCalls(parent) {
  if (!parent.__hasValidatedConnectionCalls__) {
    var calls = parent.getCallsWithValues();
    if (!RelayConnectionInterface.hasRangeCalls(calls)) {
      rangeCallEnforcer.traverse(parent, parent);
    }
    parent.__hasValidatedConnectionCalls__ = true;
  }
}

var RelayRangeCallEnforcer = (function (_RelayQueryVisitor2) {
  _inherits(RelayRangeCallEnforcer, _RelayQueryVisitor2);

  function RelayRangeCallEnforcer() {
    _classCallCheck(this, RelayRangeCallEnforcer);

    _RelayQueryVisitor2.apply(this, arguments);
  }

  RelayRangeCallEnforcer.prototype.visitField = function visitField(node, parent) {
    var schemaName = node.getSchemaName();
    !(schemaName !== EDGES && schemaName !== PAGE_INFO) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'readRelayQueryData(): The field `%s` is a connection. Fields `%s` and ' + '`%s` cannot be fetched without a `first`, `last` or `find` argument.', parent.getApplicationName(), EDGES, PAGE_INFO) : invariant(false) : undefined;
  };

  return RelayRangeCallEnforcer;
})(RelayQueryVisitor);

var rangeCallEnforcer = new RelayRangeCallEnforcer();

/**
 * Obtains a client ID (eg. `someDataID_first(10)`) for the connection
 * identified by `connectionID`. If there are no range calls on the supplied
 * `node`, then a call-less connection ID (eg. `someDataID`) will be returned
 * instead.
 */
function getConnectionClientID(node, connectionID) {
  var calls = node.getCallsWithValues();
  if (!RelayConnectionInterface.hasRangeCalls(calls)) {
    return connectionID;
  }
  return GraphQLStoreRangeUtils.getClientIDForRangeWithID(callsToGraphQL(calls), {}, connectionID);
}

/**
 * Returns the component-specific DataID stored in `state`, falling back to the
 * generic "store" DataID.
 *
 * For most nodes, the generic "store" DataID can be used for both reading out
 * of the store and writing into the result object that will be passed back to
 * the component. For connections with range calls on them the "store" and
 * "component" ID will be different because the component needs a special
 * client-ID that encodes the range calls.
 */
function getComponentDataID(state) {
  if (state.componentDataID != null) {
    return state.componentDataID;
  } else {
    return state.storeDataID;
  }
}

/**
 * Retrieves `state.data`, initializing it if necessary.
 */
function getDataObject(state) {
  var data = state.data;
  if (!data) {
    var pointer = GraphQLStoreDataHandler.createPointerWithID(getComponentDataID(state));
    data = state.data = pointer;
  }
  !(data instanceof Object) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'readRelayQueryData(): Unable to read field on non-object.') : invariant(false) : undefined;
  return data;
}

/**
 * Looks up the value identified by `key` in `state.data`.
 *
 * Pre-populates `state` with a suitable `data` objects if needed.
 */
function getDataValue(state, key) {
  var data = getDataObject(state);
  return data[key];
}

var instrumented = RelayProfiler.instrument('readRelayQueryData', readRelayQueryData);

// #7573861: Type export collides with CommonJS export in presence of
// `instrument()` call:
module.exports = instrumented;